﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Web.Script.Serialization;
using System.Web.UI;
using System.Web.UI.WebControls;
using OpenWaves.Collections;

namespace OpenWaves.Web.Controls
{
    [ToolboxData("<{0}:GoogleMap runat=server></{0}:GoogleMap>")]
    [ParseChildren(ChildrenAsProperties =  true)]
    public abstract class GoogleMapBase<T> : WebControl, INamingContainer
    {
        public IEnumerable<T> DataSource { get; set; }

        [PersistenceMode(PersistenceMode.InnerProperty)]
        public GoogleMapViewport Viewport
        {
            get { return (GoogleMapViewport) ViewState["viewport"]; }
            set { ViewState["viewport"] = value; }
        }

        [PersistenceMode(PersistenceMode.InnerProperty)]
        public ITemplate InfoWindowTemplate { get; set; }

        public bool AutoViewport
        {
            get { return (bool?)this.ViewState["autoViewport"] ?? false; }
            set { this.ViewState["autoViewport"] = value; }
        }

        public bool InitializeGoogleMap
        {
            get { return (bool?)this.ViewState["initializeGoogleMap"] ?? true; }
            set { this.ViewState["initializeGoogleMap"] = value; }
        }

        public event EventHandler<MarkerDataBoundEventArgs<T>> MarkerDataBound = delegate { };

        protected IList<GeoMarker> Markers
        {
            get
            {
                if (this.ViewState["markers"] == null)
                    this.ViewState["markers"] = new List<GeoMarker>();

                return (IList<GeoMarker>)this.ViewState["markers"];
            }
        }

        public bool IncludeGoogleMapsApiScript
        {
            get { return (bool?) this.ViewState["includeGoogleMapsApiScript"] ?? true; }
            set { this.ViewState["includeGoogleMapsApiScript"] = value; }
        }

        public bool ShowFallbackImage
        {
            get { return (bool?) this.ViewState["showFallbackImage"] ?? true; }
            set { this.ViewState["showFallbackImage"] = value; }
        }

        public string FallbackImageToolTip
        {
            get { return (string)this.ViewState["fallbackImageToolTip"] ?? String.Empty; }
            set { this.ViewState["fallbackImageToolTip"] = value; }
        }

        public string MarkerIconUrl
        {
            get { return (string)this.ViewState["markerIconUrl"]; }
            set { this.ViewState["markerIconUrl"] = value; }
        }

        protected GoogleMapBase()
            : base(HtmlTextWriterTag.Div)
        {
        }

        protected abstract string ResolveToolTip(T item);

        protected abstract double ResolveLatitude(T item);

        protected abstract double ResolveLongitude(T item);

        public override void DataBind()
        {
            base.OnDataBinding(EventArgs.Empty);

            if (this.Viewport != null)
                this.Viewport.DataBind();

            this.Markers.Clear();

            foreach (var dataItem in this.DataSource ?? Enumerable.Empty<T>())
            {
                var item = new GeoMarker();

                this.DataBindMarker(item, dataItem);

                this.Markers.Add(item);
            }
        }

        protected virtual void DataBindMarker(GeoMarker item, T dataItem)
        {
            item.Location = new GeoPoint(ResolveLatitude(dataItem), ResolveLongitude(dataItem));
            item.ToolTip = this.ResolveToolTip(dataItem);
            item.InfoWindowContent = this.CreateInfoWindowContent(dataItem);
                
            this.MarkerDataBound(this, new MarkerDataBoundEventArgs<T>(dataItem, item));
        }

        protected virtual string CreateInfoWindowContent(T dataItem)
        {
            if (this.InfoWindowTemplate == null)
            {
                return string.Empty;
            }

            using (var container = new GoogleMapInfoWindowContainer<T>(dataItem))
            {
                this.InfoWindowTemplate.InstantiateIn(container);
                this.Controls.Add(container);
                container.DataBind();

                using (var stringWriter = new StringWriter(CultureInfo.CurrentCulture))
                {
                    var writer = new HtmlTextWriter(stringWriter);
                    container.RenderControl(writer);
                    return stringWriter.ToString().Trim();
                }
            }
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            if (this.IncludeGoogleMapsApiScript)
            {
                this.Page.ClientScript.RegisterClientScriptInclude(typeof(WebResources), "googleMapScript", WebResources.GoogleMapsApiScriptUrl.ToString());
            }

            this.Page.ClientScript.RegisterClientScriptResource(typeof(GoogleMapBase<>), WebResources.GoogleMapControl);

            var declarationScript = String.Format(CultureInfo.InvariantCulture, "var {0} = new openWaves.GoogleMapControl({1});",
                this.ClientID,
                new JavaScriptSerializer().Serialize(new
                    {
                            mapContainerId = this.ClientID,
                            dataItems = this.Markers,
                            viewport = this.GetEffectiveViewport(),
                            markerIconUrl = this.MarkerIconUrl
                    }));

            this.Page.ClientScript.RegisterClientScriptBlock(typeof(GoogleMapBase<>), "declarationScript" + this.UniqueID, declarationScript, true);
            
            if (InitializeGoogleMap)
            {
                var startupScript = this.ClientID + ".initialize();";

                this.Page.ClientScript.RegisterStartupScript(typeof(GoogleMapBase<>), "startupScript" + this.UniqueID, startupScript, true);
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        protected virtual GoogleMapViewport GetEffectiveViewport()
        {
            if (this.AutoViewport || this.Viewport == null)
            {
                var bounds = CalculateBounds(this.Markers.Select(m => m.Location));
                if (bounds != null)
                    return new GoogleMapViewport(bounds);
            }

            return this.Viewport;
        }

        protected override void RenderContents(HtmlTextWriter writer)
        {
            if (this.ShowFallbackImage)
            {
                this.RenderFallbackImage(writer);
            }
        }

        private void RenderFallbackImage(HtmlTextWriter writer)
        {
            if (this.Width.IsEmpty || this.Height.IsEmpty)
                throw new InvalidOperationException("Set Width and Height properties or set ShowFallbackImage to false and specify width and height of the map with css.");

            if (this.Width.Type != UnitType.Pixel || this.Height.Type != UnitType.Pixel)
                throw new InvalidOperationException("Only pixel based values for Width and Height properties are supported when ShowFallbackImage is set to true.");

            var markersString = Markers.Select(m => string.Format(CultureInfo.InvariantCulture, "{0},{1}", m.Location.Latitude, m.Location.Longitude)).JoinStrings("|");

            var builder = WebResources.GoogleMapsStaticApiScriptUrl.GetBuilder();
            if (this.Viewport != null)
            {
                    if (this.Viewport.Center != null)
                    {
                        builder
                            .AddParameter("center", string.Format(CultureInfo.InvariantCulture, "{0},{1}", this.Viewport.Center.Latitude, this.Viewport.Center.Longitude))
                            .AddParameter("zoom", this.Viewport.Zoom.ToString(CultureInfo.InvariantCulture));
                    }
                    else if(this.Viewport.Bounds != null && this.Viewport.Bounds.TopLeft != null &&  this.Viewport.Bounds.BottomRight != null)
                    {
                        var topLeft = this.Viewport.Bounds.TopLeft;
                        var bottomRight = this.Viewport.Bounds.BottomRight;
                        builder.AddParameter("visible",
                                             string.Format(CultureInfo.InvariantCulture, "{0},{1}|{2},{3}",
                                                           topLeft.Latitude, topLeft.Longitude, bottomRight.Latitude,
                                                           bottomRight.Longitude));
                    }
            }
            
            builder
                .AddParameter("size", Width.Value + "x" + Height.Value)
                .AddParameter("markers", markersString)
                .AddParameter("sensor", "false");

            writer.AddAttribute(HtmlTextWriterAttribute.Src, builder.ToString());
            writer.AddAttribute(HtmlTextWriterAttribute.Alt, this.FallbackImageToolTip);
            writer.RenderBeginTag(HtmlTextWriterTag.Img);
            writer.RenderEndTag();
        }

        private static GeoRectangle CalculateBounds(IEnumerable<GeoPoint> points)
        {
            if(points.IsEmpty())
                return null;

            var firstPoint = points.First();
            var bounds = new GeoRectangle(firstPoint, firstPoint);

            return points.Skip(1).Aggregate(bounds, (current, point) => current.Extend(point));
        }
    }
}